from __future__ import with_statement, division

import os.path
import math

import pygame

from settings import settings

import parse
import tiles

from collision import Rect, CollisionLayer

def clamp(minimum, value, maximum):
	return max(minimum, min(value, maximum))

class ImageCache(dict):
	def __missing__(self, key):
		image = pygame.image.load(os.path.join(settings.paths.graphics, "objects/%s.png" % key)).convert_alpha()
		self[key] = image
			
		return image

class Decoration(object):
	"""An instance of an image at a speficic position and rotation"""
	graphics = ImageCache()

	def __init__(self, position, angle=0):
		self.position = position
		self.angle = angle
		
	def update(self):
		pass
		
	def draw(self, surface):
		image = pygame.transform.rotozoom(self.graphic, -self.angle, 1)
		
		x, y = self.position
		x -= image.get_width() // 2
		y -= image.get_height() // 2
		
		surface.blit(image, (x, y))

class Map(object):
	"""An animated layered tile map overlayed with decorations and populated
	with objects
	"""
	tile_size = (32, 32)
	
	@staticmethod
	def save_map(map, file):
		tile_lookup = dict(zip(map.tileset.itervalues(), map.tileset.iterkeys()))
		
		with file:
			file.writelines(("details\n",
				"\tname: \"%s\"\n" % map.name, 
				"\tauthor: \"%s\"\n" % map.author))
			
			file.writelines(("map\n"))
		
			for row in map.map_data:
				row = "".join(tile.symbol() for tile in row)
				line = "\t%s\n" % row
				
				file.writelines((line))
				
			file.writelines(("objects\n"))
			
			file.writelines("\t%s\n" % object.save(parse.variables) for object in map.objects)

	def __init__(self, level, filepath):
		self.level = level
		
		self.tileset = {}
		self.layers = []
		self.decorations = ([], [])
		self.changes = set()
		
		self._debug = False
		
		path = os.path.split(filepath)[0]
		mapfile = open(filepath)
		
		details = {}
		decoration_types = {}
		
		def parse_graphics(lines):
			def tile(symbol, *args):
				if args[0]:
					self.tileset[symbol] = tiles.tile(*args)
					return self.tileset[symbol]
				else:
					self.tileset[symbol] = None
					return None
			
			def decoration(graphic_name, fore):
				class CustomDecoration(Decoration):
					graphic = Decoration.graphics[graphic_name]
					foreground = fore
					
					def __init__(self_, *args):
						super(CustomDecoration, self_).__init__(*args)
						
						self.decorations[self_.foreground].append(self_)
				
				decoration_types[graphic_name] = CustomDecoration
				
				return CustomDecoration
			
			parse.section(lines, {"tile": tile, "decoration": decoration}, globals={"none": False, "foreground": True, "background": False})
		
		def parse_layer(lines):
			layer = []
			
			for y, line in enumerate(lines):
				row = []
				
				for x, symbol in enumerate(line):
					tile = self.tileset[symbol]
					
					if tile:
						row.append(tile((x * 32, y * 32)))
					else:
						row.append(None)
				
				layer.append(row)
			
			self.layers.append(layer)
		
		def parse_decorations(lines):
			parse.section(lines, decoration_types)
		
		parsers = {
			"details": parse.details(details),
			"graphics": parse_graphics,
			"layer": parse_layer,
			"decorations": parse_decorations}
		
		parser = parse.Parser(file=mapfile, path=path, parsers=parsers)
		parser.section("details")
		parser.section("graphics")
		parser.section("layer")
		parser.section("decorations")
		
		self.render_layers = [set() for x in xrange(8)]
		
		width, height = len(self.layers[0][0]), len(self.layers[0])
		
		self.size = (width, height)
		self.depth = len(self.layers)
		
		self.rect = Rect((0, 0, width, height))
		
		for z, layer in enumerate(self.layers):
			for y, row in enumerate(layer):
				for x, tile in enumerate(row):	
					if tile:
						tile.configure(self._configuration((x, y, z)))
		
		tile_width, tile_height = self.tile_size
		
		self.tile_render = pygame.Surface((width * tile_width, height * tile_height)).convert()
		self._render_tiles()
		
		self.decoration_renders = (
			pygame.Surface((width * tile_width, height * tile_height)).convert_alpha(),
			pygame.Surface((width * tile_width, height * tile_height)).convert_alpha())
		self._render_decorations()
		
		###
		"""self.objects = set()
		self.entries = {}
		self.selected = set()
		self.criticals = set()
		self.damages = set()
		
		self.collision_layers = {
			"units": CollisionLayer(self.size),
			"bullets": CollisionLayer(self.size),
			"sockets": CollisionLayer(self.size)}"""
		
	def _configuration(self, position):
		"""Return an 3 * 3 list of boolean values indicating which bordering
		tiles the tile at position should border with.
		"""
		
		width, height = self.size
		x, y, z = position
		
		x = clamp(0, x, width - 1)
		y = clamp(0, y, height - 1)
		
		tile = self.layers[z][y][x]
					
		configuration = (
			[False, False, False],
			[False, False, False],
			[False, False, False])
					
		for off_y in xrange(-1, 2):
			for off_x in xrange(-1, 2):
				if 0 <= x + off_x < width:
					if 0 <= y + off_y < height:
						type1 = self.layers[z][y + off_y][x + off_x].__class__
						type2 = tile.__class__
						
						value = (type1 == type2)
					else:
						value = True
				else:
					value = True
				
				configuration[off_y + 1][off_x + 1] = value
		
		return configuration
	
	def _render_tiles(self, rect=None, surface=None):
		"""Render all tiles within the rect onto the given surface. By default
		render all the tiles and onto the self.tile_render surface.
		"""
		if not surface:
			surface = self.tile_render
		if not rect:
			width, height = self.size
			rect = pygame.Rect(0, 0, width, height)
		
		x, y = rect.topleft
		width, height = rect.size
		surface.fill((0, 0, 0), (x * 32, y * 32, width * 32, height * 32))
		
		rect_x, rect_y = rect.topleft
		rect_x2, rect_y2 = rect.bottomright
		
		for layer in self.layers:
			for row in layer[rect_y:rect_y2]:
				for tile in row[rect_x:rect_x2]:	
					if tile is not None:
						tile.draw(surface)
	
	def _render_decorations(self, surfaces=None):
		"""Render all decorations onto the given surface. By default onto the 
		self.decoration_renders surfaces.
		"""
		if not surfaces:
			surfaces = self.decoration_renders
		
		for surface in surfaces:
			surface.fill((0, 0, 0, 0), surface.get_rect())
		
		for layer in self.decorations:
			for decoration in layer:
				decoration.draw(surfaces[decoration.foreground])
	
	def update(self, frame):
		"""Update each tile for animation, if it changes add the tile location
		to changes so it can be rerendered. Update every decoration"""
		for layer in self.layers:
			for y, row in enumerate(layer):
				for x, tile in enumerate(row):
					if tile is not None and tile.update(frame):
						self.changes.add((x, y))
		
		for layer in self.decorations:
			for decoration in layer:
				decoration.update()

	def draw(self, surface, viewport, offset=(0, 0)):
		for column in self.changes:
			x, y = column	
			self._render_tiles(rect=pygame.Rect(x, y, 1, 1))
		self.changes.clear()
		
		surface.blit(self.tile_render, offset, viewport)
		surface.blit(self.decoration_renders[0], offset, viewport)
		
		for layer in self.render_layers:
			for object in layer:
				object.draw(surface)
		
		surface.blit(self.decoration_renders[1], offset, viewport)
